Fix warnings in Edit in ODBEditor
[MacVim.git] / src / MacVim / edit-in-odb / src / Edit in ODBEditor.mm
blobc1e43605477cca27193cc2abe56f2c859bdef71e
1 //
2 //  Edit in ODBEditor.mm
3 //
4 //  Created by Allan Odgaard on 2005-11-26.
5 //  See LICENSE for license details
6 //
7 //  Generalized by Chris Eidhof and Eelco Lempsink from 'Edit in TextMate.mm'
9 #import <WebKit/WebKit.h>
10 #import <Carbon/Carbon.h>
11 #import <map>
12 #import "Edit in ODBEditor.h"
14 // from ODBEditorSuite.h
15 #define keyFileSender   'FSnd'
16 #define kODBEditorSuite 'R*ch'
17 #define kAEModifiedFile 'FMod'
18 #define kAEClosedFile   'FCls'
20 static NSMutableDictionary* OpenFiles;
21 static NSMutableSet* FailedFiles;
22 static NSString* ODBEditorBundleIdentifier;
23 static NSString* ODBEditorName;
25 #pragma options align=mac68k
26 struct PBX_SelectionRange
28         short unused1;          // 0 (not used)
29         short lineNum;          // line to select (<0 to specify range)
30         long startRange;        // start of selection range (if line < 0)
31         long endRange;          // end of selection range (if line < 0)
32         long unused2;           // 0 (not used)
33         long theDate;           // modification date/time
35 #pragma options align=reset
37 @implementation EditInODBEditor
38 + (void)setODBEventHandlers
40         NSAppleEventManager* eventManager = [NSAppleEventManager sharedAppleEventManager];
41         [eventManager setEventHandler:self andSelector:@selector(handleModifiedFileEvent:withReplyEvent:) forEventClass:kODBEditorSuite andEventID:kAEModifiedFile];
42         [eventManager setEventHandler:self andSelector:@selector(handleClosedFileEvent:withReplyEvent:) forEventClass:kODBEditorSuite andEventID:kAEClosedFile];
45 + (void)removeODBEventHandlers
47         NSAppleEventManager* eventManager = [NSAppleEventManager sharedAppleEventManager];
48         [eventManager removeEventHandlerForEventClass:kODBEditorSuite andEventID:kAEModifiedFile];
49         [eventManager removeEventHandlerForEventClass:kODBEditorSuite andEventID:kAEClosedFile];
52 + (BOOL)launchODBEditor
54         NSArray* array = [[NSWorkspace sharedWorkspace] launchedApplications];
55         for(unsigned i = [array count]; --i; )
56         {
57                 if([[[array objectAtIndex:i] objectForKey:@"NSApplicationBundleIdentifier"] isEqualToString:ODBEditorBundleIdentifier])
58                         return YES;
59         }
60         return [[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:ODBEditorBundleIdentifier options:0L additionalEventParamDescriptor:nil launchIdentifier:nil];
63 + (void)asyncEditStringWithOptions:(NSDictionary*)someOptions
65         NSAutoreleasePool* pool = [NSAutoreleasePool new];
67         if(![self launchODBEditor])
68                 return;
70         /* =========== */
72         NSData* targetBundleID = [ODBEditorBundleIdentifier dataUsingEncoding:NSUTF8StringEncoding];
73         NSAppleEventDescriptor* targetDescriptor = [NSAppleEventDescriptor descriptorWithDescriptorType:typeApplicationBundleID data:targetBundleID];
74         NSAppleEventDescriptor* appleEvent = [NSAppleEventDescriptor appleEventWithEventClass:kCoreEventClass eventID:kAEOpenDocuments targetDescriptor:targetDescriptor returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
75         NSAppleEventDescriptor* replyDescriptor = nil;
76         NSAppleEventDescriptor* errorDescriptor = nil;
77         AEDesc reply = { typeNull, NULL };                                                                                                              
79         NSString* fileName = [someOptions objectForKey:@"fileName"];
80         [appleEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithDescriptorType:typeFileURL data:[[[NSURL fileURLWithPath:fileName] absoluteString] dataUsingEncoding:NSUTF8StringEncoding]] forKeyword:keyDirectObject];
82         UInt32 packageType = 0, packageCreator = 0;
83         CFBundleGetPackageInfo(CFBundleGetMainBundle(), &packageType, &packageCreator);
84         [appleEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithTypeCode:packageCreator] forKeyword:keyFileSender];
86         if(int line = [[someOptions objectForKey:@"line"] intValue])
87         {
88                 PBX_SelectionRange pos = { };
89                 pos.lineNum = line;
90                 [appleEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithDescriptorType:typeChar bytes:&pos length:sizeof(pos)] forKeyword:keyAEPosition];
91         }
93         OSStatus status = AESend([appleEvent aeDesc], &reply, kAEWaitReply, kAENormalPriority, kAEDefaultTimeout, NULL, NULL);
94         if(status == noErr)
95         {
96                 replyDescriptor = [[[NSAppleEventDescriptor alloc] initWithAEDescNoCopy:&reply] autorelease];
97                 errorDescriptor = [replyDescriptor paramDescriptorForKeyword:keyErrorNumber];
98                 if(errorDescriptor != nil)
99                         status = [errorDescriptor int32Value];
100                 
101                 if(status != noErr)
102                         NSLog(@"%s error %d", _cmd, status), NSBeep();
103         }
105         [pool release];
108 + (NSString*)extensionForURL:(NSURL*)anURL
110         NSString* res = nil;
111         if(NSString* urlString = [anURL absoluteString])
112         {
113                 NSString* path = [[NSBundle bundleForClass:[self class]] pathForResource:@"url map" ofType:@"plist"];
114                 NSMutableDictionary* map = [NSMutableDictionary dictionaryWithContentsOfFile:path];
116                 NSString* customBindingsPath = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"Preferences/org.slashpunt.edit_in_odbeditor.plist"];
117                 if(NSDictionary* associations = [[NSDictionary dictionaryWithContentsOfFile:customBindingsPath] objectForKey:@"URLAssociations"])
118                         [map addEntriesFromDictionary:associations];
120                 unsigned longestMatch = 0;
121                 NSEnumerator* enumerator = [map keyEnumerator];
122                 while(NSString* key = [enumerator nextObject])
123                 {
124                         if([urlString rangeOfString:key].location != NSNotFound && [key length] > longestMatch)
125                         {
126                                 res = [map objectForKey:key];
127                                 longestMatch = [key length];
128                         }
129                 }
130         }
131         return res;
134 + (void)externalEditString:(NSString*)aString startingAtLine:(int)aLine forView:(NSView*)aView
136         [self externalEditString:aString startingAtLine:aLine forView:aView withObject:nil];
139 + (void)externalEditString:(NSString*)aString startingAtLine:(int)aLine forView:(NSView*)aView withObject:(NSObject*)anObject
141         Class cl = NSClassFromString(@"WebFrameView");
143         NSURL* url = nil;
144         for(NSView* view = aView; view && !url && cl; view = [view superview])
145         {
146                 if([view isKindOfClass:cl])
147                         url = [[[[(WebFrameView*)view webFrame] dataSource] mainResource] URL];
148         }
150         NSString* basename = [[[[aView window] title] componentsSeparatedByString:@"/"] componentsJoinedByString:@"-"] ?: @"untitled";
151         NSString* extension = [self extensionForURL:url] ?: [[[[NSWorkspace sharedWorkspace] activeApplication] objectForKey:@"NSApplicationName"] lowercaseString];
152         NSString* fileName = [NSString stringWithFormat:@"%@/%@.%@", NSTemporaryDirectory(), basename, extension];
153         for(unsigned i = 2; [[NSFileManager defaultManager] fileExistsAtPath:fileName]; i++)
154                 fileName = [NSString stringWithFormat:@"%@/%@ %u.%@", NSTemporaryDirectory(), basename, i, extension];
156         [[aString dataUsingEncoding:NSUTF8StringEncoding] writeToFile:fileName atomically:NO];
157         fileName = [fileName stringByStandardizingPath];
159         NSDictionary* options = [NSDictionary dictionaryWithObjectsAndKeys:
160                 aString,                         @"string",
161                 aView,                           @"view",
162                 fileName,                        @"fileName",
163                 [NSNumber numberWithInt:aLine],  @"line",
164                 anObject,                        @"object", /* last since anObject might be nil */
165                 nil];
167         [OpenFiles setObject:options forKey:[fileName precomposedStringWithCanonicalMapping]];
168         if([OpenFiles count] == 1)
169                 [self setODBEventHandlers];
170         [NSThread detachNewThreadSelector:@selector(asyncEditStringWithOptions:) toTarget:self withObject:options];
173 + (void)handleModifiedFileEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent
175         NSAppleEventDescriptor* fileURL = [[event paramDescriptorForKeyword:keyDirectObject] coerceToDescriptorType:typeFileURL];
176         NSString* urlString = [[[NSString alloc] initWithData:[fileURL data] encoding:NSUTF8StringEncoding] autorelease];
177         NSString* fileName = [[[NSURL URLWithString:urlString] path] stringByStandardizingPath];
178         NSDictionary* options = [OpenFiles objectForKey:[fileName precomposedStringWithCanonicalMapping]];
179         NSView* view = [options objectForKey:@"view"];
181         if([view window])
182         {
183                 if ([view respondsToSelector:@selector(odbEditorDidModifyString:withObject:)])
184                 {
185                         NSString* newString = [[[NSString alloc] initWithData:[NSData dataWithContentsOfFile:fileName] encoding:NSUTF8StringEncoding] autorelease];
186                         NSObject* anObject = [options objectForKey:@"object"];
187                         [view performSelector:@selector(odbEditorDidModifyString:withObject:) withObject:newString withObject:anObject];
188                         [FailedFiles removeObject:fileName];
189                         fileName = nil;
190                 }
191                 else if([view respondsToSelector:@selector(odbEditorDidModifyString:)])
192                 {
193                         NSString* newString = [[[NSString alloc] initWithData:[NSData dataWithContentsOfFile:fileName] encoding:NSUTF8StringEncoding] autorelease];
194                         [view performSelector:@selector(odbEditorDidModifyString:) withObject:newString];
195                         [FailedFiles removeObject:fileName];
196                         fileName = nil;
197                 }
198         }
199         if (fileName)
200         {
201                 [FailedFiles addObject:fileName];
202                 NSLog(@"%s view %p, %@, window %@", _cmd, view, view, [view window]);
203                 NSLog(@"%s file name %@, options %@", _cmd, fileName, [options description]);
204                 NSLog(@"%s all %@", _cmd, [OpenFiles description]);
205                 NSBeep();
206         }
209 + (void)handleClosedFileEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent
211         NSAppleEventDescriptor* fileURL = [[event paramDescriptorForKeyword:keyDirectObject] coerceToDescriptorType:typeFileURL];
212         NSString* urlString = [[[NSString alloc] initWithData:[fileURL data] encoding:NSUTF8StringEncoding] autorelease];
213         NSString* fileName = [[[NSURL URLWithString:urlString] path] stringByStandardizingPath];
215         if([FailedFiles containsObject:fileName])
216         {
217                 if([[NSFileManager defaultManager] fileExistsAtPath:fileName])
218                         [[NSWorkspace sharedWorkspace] selectFile:fileName inFileViewerRootedAtPath:[fileName stringByDeletingLastPathComponent]];
219                 [FailedFiles removeObject:fileName];
220         }
221         else
222         {
223                 [[NSFileManager defaultManager] removeFileAtPath:fileName handler:nil];
224                 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
225         }
227         [OpenFiles removeObjectForKey:[fileName precomposedStringWithCanonicalMapping]];
228         if([OpenFiles count] == 0)
229                 [self removeODBEventHandlers];
232 + (NSMenu*)findEditMenu
234         NSMenu* mainMenu = [NSApp mainMenu];
235         std::map<size_t, NSMenu*> ranked;
236         for(int i = 0; i != [mainMenu numberOfItems]; i++)
237         {
238                 NSMenu* candidate = [[mainMenu itemAtIndex:i] submenu];
239                 static SEL const actions[] = { @selector(undo:), @selector(redo:), @selector(cut:), @selector(copy:), @selector(paste:), @selector(delete:), @selector(selectAll:) };
240                 size_t score = 0;
241                 for(int j = 0; j != sizeof(actions)/sizeof(actions[0]); j++)
242                 {
243                         if(-1 != [candidate indexOfItemWithTarget:nil andAction:actions[j]])
244                                 score++;
245                 }
247                 if(score > 0 && ranked.find(score) == ranked.end())
248                         ranked[score] = candidate;
249         }
250         return ranked.empty() ? nil : (--ranked.end())->second;
253 + (void)installMenuItem:(id)sender
255         if(NSMenu* editMenu = [self findEditMenu])
256         {
257                 [editMenu addItem:[NSMenuItem separatorItem]];
258                 NSString* ellips = [NSString stringWithUTF8String:"\xe2\x80\xa6"]; // utf-8 for the '...' character (literal utf8 is not allowed in source code)
259                 NSMenuItem *menuItem = [editMenu addItemWithTitle:[NSString stringWithFormat:@"Edit in %@%@", ODBEditorName, ellips] action:@selector(editInODBEditor:) keyEquivalent:@"E"];
260                 [menuItem setKeyEquivalentModifierMask:NSControlKeyMask | NSCommandKeyMask];
261         }
264 + (void)load
266         OpenFiles = [NSMutableDictionary new];
267         FailedFiles = [NSMutableSet new];
268         NSString* mainBundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; // reads app we're used inside off
269         NSString* bundleIdentifier = @"org.slashpunt.edit_in_odbeditor"; // XXX Should this be hardcoded?
270         NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
271         [defaults addSuiteNamed:bundleIdentifier];
272         NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
273                 @"NO",        @"DisableEditInODBEditorMenuItem",
274                 @"",          @"ODBEditorBundleIdentifier",
275                 @"<Unknown>", @"ODBEditorName",
276                 nil];
278         [defaults registerDefaults:appDefaults];        
279         
280         ODBEditorBundleIdentifier = [[defaults stringForKey:@"ODBEditorBundleIdentifier"] retain] ?: @"";
281         ODBEditorName             = [[defaults stringForKey:@"ODBEditorName"] retain] ?: @"<Unknown>";
282         if([defaults boolForKey:@"DisableEditInODBEditorMenuItem"] == NO
283                 && ![ODBEditorBundleIdentifier isEqualToString:@""]
284                 && ![ODBEditorBundleIdentifier isEqualToString:mainBundleIdentifier])
285                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(installMenuItem:) name:NSApplicationDidFinishLaunchingNotification object:[NSApplication sharedApplication]];
287 @end